aboutsummaryrefslogtreecommitdiff
path: root/src/app/(main)/websites/[websiteId]/(reports)/attribution
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-24 13:09:50 +0000
committerFuwn <[email protected]>2026-01-24 13:09:50 +0000
commit396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch)
treeb9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/(main)/websites/[websiteId]/(reports)/attribution
downloadumami-main.tar.xz
umami-main.zip
Initial commitHEADmain
Created from https://vercel.com/new
Diffstat (limited to 'src/app/(main)/websites/[websiteId]/(reports)/attribution')
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx128
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx63
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx12
3 files changed, 203 insertions, 0 deletions
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx
new file mode 100644
index 0000000..264923a
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx
@@ -0,0 +1,128 @@
+import { Column, Grid } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { useMessages, useResultQuery } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { percentFilter } from '@/lib/filters';
+import { formatLongNumber } from '@/lib/format';
+
+export interface AttributionProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ model: string;
+ type: string;
+ step: string;
+ currency?: string;
+}
+
+export function Attribution({
+ websiteId,
+ startDate,
+ endDate,
+ model,
+ type,
+ step,
+ currency,
+}: AttributionProps) {
+ const { data, error, isLoading } = useResultQuery<any>('attribution', {
+ websiteId,
+ startDate,
+ endDate,
+ model,
+ type,
+ step,
+ });
+
+ const { formatMessage, labels } = useMessages();
+
+ const { pageviews, visitors, visits } = data?.total || {};
+
+ const metrics = data
+ ? [
+ {
+ value: visitors,
+ label: formatMessage(labels.visitors),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: formatMessage(labels.visits),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: pageviews,
+ label: formatMessage(labels.views),
+ formatValue: formatLongNumber,
+ },
+ ]
+ : [];
+
+ function AttributionTable({ data = [], title }: { data: any; title: string }) {
+ const attributionData = percentFilter(
+ data.map(({ name, value }) => ({
+ x: name,
+ y: Number(value),
+ })),
+ );
+
+ return (
+ <ListTable
+ title={title}
+ metric={formatMessage(currency ? labels.revenue : labels.visitors)}
+ currency={currency}
+ data={attributionData.map(({ x, y, z }: { x: string; y: number; z: number }) => ({
+ label: x,
+ count: y,
+ percent: z,
+ }))}
+ />
+ );
+ }
+
+ return (
+ <LoadingPanel data={data} isLoading={isLoading} error={error}>
+ {data && (
+ <Column gap>
+ <MetricsBar>
+ {metrics?.map(({ label, value, formatValue }) => {
+ return (
+ <MetricCard key={label} value={value} label={label} formatValue={formatValue} />
+ );
+ })}
+ </MetricsBar>
+ <SectionHeader title={formatMessage(labels.sources)} />
+ <Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
+ <Panel>
+ <AttributionTable data={data?.referrer} title={formatMessage(labels.referrer)} />
+ </Panel>
+ <Panel>
+ <AttributionTable data={data?.paidAds} title={formatMessage(labels.paidAds)} />
+ </Panel>
+ </Grid>
+ <SectionHeader title="UTM" />
+ <Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
+ <Panel>
+ <AttributionTable data={data?.utm_source} title={formatMessage(labels.sources)} />
+ </Panel>
+ <Panel>
+ <AttributionTable data={data?.utm_medium} title={formatMessage(labels.medium)} />
+ </Panel>
+ <Panel>
+ <AttributionTable data={data?.utm_cmapaign} title={formatMessage(labels.campaigns)} />
+ </Panel>
+ <Panel>
+ <AttributionTable data={data?.utm_content} title={formatMessage(labels.content)} />
+ </Panel>
+ <Panel>
+ <AttributionTable data={data?.utm_term} title={formatMessage(labels.terms)} />
+ </Panel>
+ </Grid>
+ </Column>
+ )}
+ </LoadingPanel>
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx
new file mode 100644
index 0000000..48611c4
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx
@@ -0,0 +1,63 @@
+'use client';
+import { Column, Grid, ListItem, SearchField, Select } from '@umami/react-zen';
+import { useState } from 'react';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { Attribution } from './Attribution';
+
+export function AttributionPage({ websiteId }: { websiteId: string }) {
+ const [model, setModel] = useState('first-click');
+ const [type, setType] = useState('path');
+ const [step, setStep] = useState('/');
+ const { formatMessage, labels } = useMessages();
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+
+ return (
+ <Column gap="6">
+ <WebsiteControls websiteId={websiteId} />
+ <Grid columns={{ xs: '1fr', md: '1fr 1fr 1fr' }} gap>
+ <Column>
+ <Select
+ label={formatMessage(labels.model)}
+ value={model}
+ defaultValue={model}
+ onChange={setModel}
+ >
+ <ListItem id="first-click">{formatMessage(labels.firstClick)}</ListItem>
+ <ListItem id="last-click">{formatMessage(labels.lastClick)}</ListItem>
+ </Select>
+ </Column>
+ <Column>
+ <Select
+ label={formatMessage(labels.type)}
+ value={type}
+ defaultValue={type}
+ onChange={setType}
+ >
+ <ListItem id="path">{formatMessage(labels.viewedPage)}</ListItem>
+ <ListItem id="event">{formatMessage(labels.triggeredEvent)}</ListItem>
+ </Select>
+ </Column>
+ <Column>
+ <SearchField
+ label={formatMessage(labels.conversionStep)}
+ value={step}
+ defaultValue={step}
+ onSearch={setStep}
+ delay={1000}
+ />
+ </Column>
+ </Grid>
+ <Attribution
+ websiteId={websiteId}
+ startDate={startDate}
+ endDate={endDate}
+ model={model}
+ type={type}
+ step={step}
+ />
+ </Column>
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx
new file mode 100644
index 0000000..1368d4b
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { AttributionPage } from './AttributionPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return <AttributionPage websiteId={websiteId} />;
+}
+
+export const metadata: Metadata = {
+ title: 'Attribution',
+};